Skip to content

Comments

Add san cli#193

Open
tspenov wants to merge 2 commits intomasterfrom
add-san-cli
Open

Add san cli#193
tspenov wants to merge 2 commits intomasterfrom
add-san-cli

Conversation

@tspenov
Copy link
Collaborator

@tspenov tspenov commented Jan 26, 2026

san --help                    # Show all commands
san --version                 # Show version

# Configuration
san config set-key <key>      # Store API key
san config show               # Show current config
san config path               # Show config file location
san config clear              # Remove stored API key

# Discovery
san metrics                   # List all metrics
san metrics --slug bitcoin    # Metrics for specific asset
san projects                  # List all projects

# Data Fetching
san get <metric> --slug <slug> [--from DATE] [--to DATE] [--interval INT] [-f FORMAT]
san get-many <metric> --slugs <slug1,slug2> [options]

# Rate Limits & Complexity
san rate-limit                # API calls remaining
san api-calls                 # API call history
san complexity <metric>       # Query complexity check

Summary by CodeRabbit

  • New Features

    • Introduced a complete CLI interface with commands for configuration management, metrics discovery, data retrieval, and API diagnostics.
    • Added support for multiple output formats (JSON, CSV, table) across CLI commands.
    • Configuration management for storing and managing API keys.
  • Chores

    • Version bumped to 0.13.0.
    • Added new CLI dependency support.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive CLI interface to the sanpy library built with Typer. It adds configuration management for API keys, commands for metrics and projects discovery, data fetching and rate limit monitoring, multiple output formatters (JSON, CSV, table), and extensive unit and integration tests. A minor notebook import cleanup is also included.

Changes

Cohort / File(s) Summary
CLI Core Implementation
san/cli.py, san/cli_config.py, san/cli_formatters.py
New modules providing complete CLI with subcommands for config (set-key, show, path, clear), discovery (metrics, projects), data fetching (get, get-many), monitoring (rate-limit, api-calls, complexity), and output formatting (json, csv, table). Config module manages cross-platform API key storage. Formatters handle DataFrame, list, dict, and API call rendering.
Test Suite
san/tests/test_cli.py, san/tests/test_cli_integration.py
New comprehensive test files with unit tests using mocked API responses and integration tests against live endpoints. Cover command execution, output validation, error handling, and multiple output formats.
Package Configuration
setup.py
Version bump (0.12.4 → 0.13.0), added typer>=0.9.0 dependency, and console script entry point mapping "san" to CLI app.
Example Maintenance
examples/extras/event_study.ipynb
Removed unused from datetime import date import.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as CLI Command Handler
    participant Config as Config Module
    participant API as Santiment API
    participant Formatter as Output Formatter
    
    User->>CLI: Execute command (e.g., get metric)
    CLI->>Config: Get stored or env API key
    Config-->>CLI: Return API key
    CLI->>API: Make API request with key
    API-->>CLI: Return data (DataFrame/dict)
    CLI->>Formatter: Format data (json/csv/table)
    Formatter-->>CLI: Formatted output
    CLI->>User: Display output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • IvanIvanoff

Poem

🐰 CLI hops into the warren, with config in its burrow,
Typer commands fetch metrics, no need to borrow,
Formatters paint tables, JSON, and CSV spreads,
Tests verify every command before dinner spreads,
Sanpy's CLI adventure begins! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add san cli' directly describes the main change: introducing a complete CLI module for the sanpy project with multiple commands and subcommands.
Docstring Coverage ✅ Passed Docstring coverage is 98.63% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@san/tests/test_cli_integration.py`:
- Around line 130-131: The test currently uses a weak check `assert "datetime"
in data[0] or "value" in data[0]`; update the assertion to require both fields
by changing it to `assert "datetime" in data[0] and "value" in data[0]` (i.e.,
validate that both keys exist on the first element of `data`), so the structure
check for `data[0]` verifies both expected fields are present.

In `@san/tests/test_cli.py`:
- Around line 24-29: The fixture mock_api_key is unused; either remove the
unused pytest fixture definition named mock_api_key or attach it to tests that
require it by adding mock_api_key as a test parameter; if keeping it, ensure the
patch target "san.ApiConfig" is correct and referenced by tests (e.g., include
mock_api_key in test function signatures that rely on san.ApiConfig.api_key)
otherwise delete the entire mock_api_key fixture block to eliminate dead code.
🧹 Nitpick comments (8)
san/cli_config.py (2)

24-32: Potential type inconsistency with Path conversion.

When APPDATA or XDG_CONFIG_HOME is not set, Path.home() returns a Path object, but when the env var exists, os.environ.get() returns a string. While Path(base) on line 31 handles this, it would be cleaner to ensure consistent types.

♻️ Suggested improvement
 def get_config_dir() -> Path:
     """Get the platform-appropriate config directory."""
     if os.name == "nt":  # Windows
-        base = os.environ.get("APPDATA", Path.home())
+        base = Path(os.environ.get("APPDATA", str(Path.home())))
     else:  # Linux/Mac
-        base = os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
+        xdg_config = os.environ.get("XDG_CONFIG_HOME")
+        base = Path(xdg_config) if xdg_config else Path.home() / ".config"
 
-    config_dir = Path(base) / "sanpy"
+    config_dir = base / "sanpy"
     return config_dir

56-65: Silent error handling may hide config file corruption.

When _load_config encounters a JSONDecodeError, it silently returns an empty dict. This could mask a corrupted config file from the user. Consider logging a warning or informing the user that their config was reset.

♻️ Suggested improvement
 def _load_config() -> dict:
-    """Load config from file, return empty dict if doesn't exist."""
+    """Load config from file, return empty dict if doesn't exist or is corrupted."""
     config_path = get_config_path()
     if config_path.exists():
         try:
             with open(config_path, "r") as f:
                 return json.load(f)
-        except (json.JSONDecodeError, IOError):
+        except json.JSONDecodeError:
+            # Config file is corrupted, return empty and let next save overwrite
+            import sys
+            print(f"Warning: Config file '{config_path}' is corrupted, using defaults.", file=sys.stderr)
+            return {}
+        except IOError:
             return {}
     return {}
san/tests/test_cli_integration.py (1)

321-338: Consider adding assertion for error message content.

The test verifies the exit code and that "error" appears in stdout, but doesn't validate that the error is specifically about the invalid slug. This could help catch regressions in error messaging.

san/cli_formatters.py (2)

49-54: CSV and table formats produce identical output for lists.

The csv and table branches produce the same output ("\n".join(items)). For true CSV format, consider adding a header or quoting values. If this is intentional, a comment would clarify the design choice.


100-104: Inconsistent CSV escaping - consider using csv module.

format_dict uses the csv module for proper escaping, but format_api_calls uses string formatting. If a date string contains commas or special characters, this could produce malformed CSV.

♻️ Suggested fix
     elif fmt == "csv":
-        lines = ["date,api_calls"]
-        for date, count in calls:
-            lines.append(f"{date},{count}")
-        return "\n".join(lines)
+        buffer = io.StringIO()
+        writer = csv.writer(buffer, quoting=csv.QUOTE_MINIMAL)
+        writer.writerow(["date", "api_calls"])
+        for date, count in calls:
+            writer.writerow([date, count])
+        return buffer.getvalue()
san/cli.py (2)

72-78: Consider security implications of displaying config path.

Echoing the full config path after saving the API key is helpful for transparency, but ensure users understand the key is stored in plaintext.

♻️ Optional enhancement for security awareness
 def config_set_key(
     api_key: Annotated[str, typer.Argument(help="Your Santiment API key")],
 ) -> None:
     """Store API key in config file."""
     set_api_key(api_key)
-    output(f"API key saved to {get_config_path()}")
+    output(f"API key saved to {get_config_path()}")
+    output("Note: The API key is stored in plaintext.")

164-188: --slug is a required option - consider making it a positional argument.

The slug parameter is marked as required (no default value), which is correct. However, for better UX, consider whether slug should be a positional argument like metric, enabling san get price_usd bitcoin instead of san get price_usd --slug bitcoin.

setup.py (1)

19-19: Update the typer version constraint.

The typer>=0.9.0 constraint is significantly outdated. The latest stable version is 0.21.1 (released Jan 6, 2026). Consider updating the constraint to typer>=0.21.0 or higher to benefit from bug fixes, performance improvements, and new features from recent releases.

Comment on lines +130 to +131
# Check structure - should have datetime and value
assert "datetime" in data[0] or "value" in data[0]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Weak assertion - consider checking for both fields.

The assertion "datetime" in data[0] or "value" in data[0] will pass if only one field exists. This should verify that both expected fields are present.

💚 Suggested fix
-    # Check structure - should have datetime and value
-    assert "datetime" in data[0] or "value" in data[0]
+    # Check structure - should have datetime and value
+    assert "datetime" in data[0], "Expected 'datetime' field in JSON output"
+    assert "value" in data[0], "Expected 'value' field in JSON output"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Check structure - should have datetime and value
assert "datetime" in data[0] or "value" in data[0]
# Check structure - should have datetime and value
assert "datetime" in data[0], "Expected 'datetime' field in JSON output"
assert "value" in data[0], "Expected 'value' field in JSON output"
🤖 Prompt for AI Agents
In `@san/tests/test_cli_integration.py` around lines 130 - 131, The test currently
uses a weak check `assert "datetime" in data[0] or "value" in data[0]`; update
the assertion to require both fields by changing it to `assert "datetime" in
data[0] and "value" in data[0]` (i.e., validate that both keys exist on the
first element of `data`), so the structure check for `data[0]` verifies both
expected fields are present.

Comment on lines +24 to +29
@pytest.fixture
def mock_api_key():
"""Set up a mock API key for tests."""
with patch("san.ApiConfig") as mock_config:
mock_config.api_key = "test_api_key_12345"
yield mock_config
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check imports and usage in test_cli.py
echo "=== Imports and fixture usage in test_cli.py ==="
head -40 san/tests/test_cli.py

echo ""
echo "=== ApiConfig imports and usage in cli.py ==="
rg -n "ApiConfig|from san|import san" san/cli.py | head -20

echo ""
echo "=== ApiConfig definition ==="
rg -n "class ApiConfig" san/

echo ""
echo "=== How mock_api_key fixture is used in tests ==="
rg -n "mock_api_key" san/tests/test_cli.py | head -10

Repository: santiment/sanpy

Length of output: 1505


🏁 Script executed:

#!/bin/bash

# Find tests that use mock_api_key parameter
echo "=== Tests using mock_api_key fixture ==="
rg -B2 "def test.*mock_api_key" san/tests/test_cli.py

echo ""
echo "=== Check if mock_api_key is imported from anywhere ==="
rg "from.*mock_api_key|import.*mock_api_key" san/tests/test_cli.py

echo ""
echo "=== Show full imports section of cli.py to understand san module ==="
head -30 san/cli.py

echo ""
echo "=== Check what san module is ==="
cat san/__init__.py

Repository: santiment/sanpy

Length of output: 2412


🏁 Script executed:

#!/bin/bash

# Search for all test function definitions
echo "=== All test functions in test_cli.py ==="
rg "^def test_" san/tests/test_cli.py | head -20

echo ""
echo "=== Check entire fixture section and how fixtures are used ==="
rg -A30 "@pytest.fixture" san/tests/test_cli.py

echo ""
echo "=== Check how mock_api_key is actually used in tests ==="
rg -C5 "mock_api_key" san/tests/test_cli.py

Repository: santiment/sanpy

Length of output: 2738


🏁 Script executed:

#!/bin/bash

# Search more carefully - maybe fixture is used but with autouse or other method
echo "=== Check if mock_api_key fixture has autouse ==="
grep -n "autouse\|mock_api_key" san/tests/test_cli.py

echo ""
echo "=== Count total lines to see complete file ==="
wc -l san/tests/test_cli.py

echo ""
echo "=== Check the entire fixtures section and beyond ==="
sed -n '24,100p' san/tests/test_cli.py

Repository: santiment/sanpy

Length of output: 2492


🏁 Script executed:

#!/bin/bash

# Check conftest.py files
echo "=== Look for conftest.py files ==="
find san/tests -name "conftest.py" -exec cat {} \;

echo ""
echo "=== Check if mock_api_key is referenced anywhere else in tests ==="
rg "mock_api_key" san/tests/

echo ""
echo "=== Let's verify the patch mechanism by checking mock usage in existing tests ==="
sed -n '100,150p' san/tests/test_cli.py

Repository: santiment/sanpy

Length of output: 2228


🏁 Script executed:

#!/bin/bash

# Let's verify the exact import structure and how patching works
echo "=== Verify san module structure ==="
python3 << 'PYTHON'
import sys
sys.path.insert(0, '/tmp/repo')

# Simulate the imports as they happen in cli.py
import san
print(f"san.ApiConfig: {san.ApiConfig}")
print(f"san.ApiConfig is from san.api_config: {san.ApiConfig.__module__}")

# Verify that patching at "san.ApiConfig" would work
from unittest.mock import patch

# This is what the fixture does
with patch("san.ApiConfig") as mock_config:
    mock_config.api_key = "test_key"
    # Now when code does import san; san.ApiConfig.api_key
    # Would it be affected?
    print(f"Inside patch context: san.ApiConfig = {san.ApiConfig}")
    print(f"san.ApiConfig.api_key = {san.ApiConfig.api_key}")
PYTHON

echo ""
echo "=== Verify the actual test runs and behavior ==="
cd /tmp/repo && python3 -m pytest san/tests/test_cli.py::test_version -v 2>&1 | head -30

Repository: santiment/sanpy

Length of output: 145


🏁 Script executed:

#!/bin/bash

# Simpler approach - analyze the code structure without running tests
echo "=== Verify fixture is not used by any test ==="
rg "def test_.*\(.*mock_api_key" san/tests/test_cli.py

echo ""
echo "=== Check how other fixtures are used (with actual parameters) ==="
grep -n "def test_.*(" san/tests/test_cli.py | head -10

echo ""
echo "=== Analyze the patch target logic ==="
echo "Looking at imports in test_cli.py:"
head -15 san/tests/test_cli.py

echo ""
echo "Looking at patch in fixture:"
sed -n '24,29p' san/tests/test_cli.py

echo ""
echo "=== Show how other mocks patch things ==="
grep -n "@patch" san/tests/test_cli.py | head -10

echo ""
echo "=== Example of `@patch` usage in the file ==="
sed -n '138,145p' san/tests/test_cli.py

Repository: santiment/sanpy

Length of output: 1768


🏁 Script executed:

#!/bin/bash

# Final verification: is the fixture genuinely unused?
echo "=== Search for mock_api_key in entire test directory ==="
rg "mock_api_key" san/tests/

echo ""
echo "=== Check git history to see if this fixture was recently added/removed usage ==="
git log --oneline -n 20 san/tests/test_cli.py 2>/dev/null || echo "Git not available"

echo ""
echo "=== Verify patch target comprehensively ==="
echo "When cli.py does 'import san', it gets san module"
echo "san/__init__.py imports ApiConfig like this:"
grep "from.*ApiConfig\|import.*ApiConfig" san/__init__.py

echo ""
echo "So cli.py accesses it as san.ApiConfig, which means patching 'san.ApiConfig' is correct."
echo ""
echo "Let's verify the other `@patch` decorators use the same pattern:"
grep "@patch" san/tests/test_cli.py | head -5

Repository: santiment/sanpy

Length of output: 737


Fixture is unused and should be removed.

The mock_api_key fixture is defined but never referenced by any test—no test function includes it as a parameter. Since it's not being used, either add it to test functions that need it (e.g., by including mock_api_key as a parameter) or remove it entirely as dead code.

Note: The patch target "san.ApiConfig" is actually correct, as cli.py imports the san module and accesses san.ApiConfig.api_key.

🤖 Prompt for AI Agents
In `@san/tests/test_cli.py` around lines 24 - 29, The fixture mock_api_key is
unused; either remove the unused pytest fixture definition named mock_api_key or
attach it to tests that require it by adding mock_api_key as a test parameter;
if keeping it, ensure the patch target "san.ApiConfig" is correct and referenced
by tests (e.g., include mock_api_key in test function signatures that rely on
san.ApiConfig.api_key) otherwise delete the entire mock_api_key fixture block to
eliminate dead code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant